How to migrate ASP.NET Web Forms themes (App_Themes) to Blazor using BWFC auto-discovery
When migrating an ASP.NET Web Forms application to Blazor using BWFC, the application's themes (stored in App_Themes/ folder) need to be migrated to the Blazor project. Web Forms themes consist of:
.skin files: ASP.NET control skin definitions with default property values.css files: Theme CSS filesimages/ folder: Theme-related imagesBWFC provides automatic theme discovery and registration. When you copy the theme folder to the Blazor project, AddBlazorWebFormsComponents() auto-discovers .skin files, parses them via SkinFileParser, registers the theme in DI, and ThemeProvider automatically injects theme CSS into the layout. No manual registration code is required.
Locate the App_Themes/ folder in the source Web Forms project:
MyWebFormsApp/
App_Themes/
MyTheme/
GridView.skin
Button.skin
MyTheme.css
images/
button-bg.png
DarkTheme/
GridView.skin
Button.skin
DarkTheme.cssEach subfolder under App_Themes/ is a separate theme.
Copy the entire App_Themes/ folder from the Web Forms project to wwwroot/App_Themes/ in the Blazor project:
Source: MyWebFormsApp/App_Themes/
Target: BlazorApp/wwwroot/App_Themes/Important: Preserve the exact folder structure:
When the Blazor application starts and AddBlazorWebFormsComponents() is called in Program.cs:
wwwroot/App_Themes/ for theme foldersSkinFileParser.ParseThemeFolder() reads all .skin files and converts them to ControlSkin objectsThemeConfiguration is registered in the dependency injection container.css files in the theme folder are auto-discoveredIn your Blazor layout (typically App.razor or MainLayout.razor), use ThemeProvider:
<ThemeProvider>
@Body
</ThemeProvider>ThemeProvider automatically:
<head> via HeadContentIf a .skin file defines a named skin with SkinID, you must explicitly pass the SkinID parameter to the component:
<!-- Web Forms: uses default Button skin automatically -->
<asp:Button runat="server" Text="Normal Button" />
<!-- Web Forms: uses DangerButton skin -->
<asp:Button runat="server" SkinID="DangerButton" Text="Danger Button" />
<!-- Blazor: default skin applied automatically -->
<Button Text="Normal Button" />
<!-- Blazor: must explicitly pass SkinID -->
<Button Text="Danger Button" SkinID="DangerButton" />If the .skin file does not define a SkinID attribute, the component uses the default (unnamed) skin.
Web Forms:
App_Themes/
BlueTheme/
Button.skin # <asp:Button runat="server" ForeColor="White" BackColor="Navy" />
GridView.skin # <asp:GridView runat="server" CssClass="grid" HeaderStyle-BackColor="Navy" />
BlueTheme.css # .grid { border: 1px solid #ccc; }Migration steps:
App_Themes/BlueTheme/ → wwwroot/App_Themes/BlueTheme/Program.cs calls AddBlazorWebFormsComponents()<ThemeProvider> in layoutResult: All Button, GridView, and other components with skins now render with BlueTheme defaults. CSS is injected automatically.
If the Web Forms app has multiple themes:
App_Themes/
BlueTheme/
*.skin
BlueTheme.css
GreenTheme/
*.skin
GreenTheme.cssMigration:
wwwroot/App_Themes/To use a non-default theme, use a custom options builder (see Manual Override below).
Web Forms .skin file:
<asp:Button runat="server" CssClass="btn" />
<asp:Button runat="server" SkinID="DangerButton" BackColor="Red" CssClass="btn btn-danger" />
<asp:Button runat="server" SkinID="SuccessButton" BackColor="Green" CssClass="btn btn-success" />Blazor usage:
<!-- Default skin -->
<Button Text="Click Me" />
<!-- DangerButton skin -->
<Button Text="Delete" SkinID="DangerButton" />
<!-- SuccessButton skin -->
<Button Text="Save" SkinID="SuccessButton" />If the source Web Forms application does not have an App_Themes/ folder, there are no skins to migrate. Components render without theme defaults. This is normal for Web Forms apps that don't use themes.
If wwwroot/App_Themes/MyTheme/ contains only .css files and no .skin files:
ThemeProviderIf a theme folder has multiple .css files:
wwwroot/App_Themes/MyTheme/
theme.css
components.css
utilities.cssAll CSS files are auto-discovered and injected in the <head> by ThemeProvider. Load order is alphabetical.
If you need to store themes in a location other than wwwroot/App_Themes/, configure AddBlazorWebFormsComponents():
builder.Services.AddBlazorWebFormsComponents(options =>
{
options.ThemesPath = "my-custom-themes"; // relative to wwwroot
});By default, BWFC uses StyleSheetTheme mode: theme defaults are applied only if the component property is not explicitly set. Explicit component properties always win.
<!-- Uses theme default BackColor -->
<Button Text="Button 1" />
<!-- Ignores theme BackColor, uses explicit Red -->
<Button Text="Button 2" BackColor="Red" />To change to Theme mode (theme wins over explicit properties), configure:
builder.Services.AddBlazorWebFormsComponents(options =>
{
options.ThemeMode = ThemeMode.Theme; // theme wins over explicit
});Theme mode is rarely used and not recommended unless you have a specific reason to override explicit properties.
Do NOT manually register skins in Program.cs. Let auto-discovery do it:
// ❌ WRONG: Manual registration is unnecessary
var config = new ThemeConfiguration();
config.Skins["Button"].Add("DangerButton", new ControlSkin { ... });
builder.Services.AddSingleton(config);Instead: Ensure .skin files are in wwwroot/App_Themes/ and let AddBlazorWebFormsComponents() auto-discover.
Do NOT hand-edit .skin files after copying them. They are XML-based and parsing is strict:
<!-- ❌ WRONG: Unbalanced tags, comments, invalid nesting -->
<asp:Button runat="server" CssClass="btn"
<asp:GridView runat="server" <!-- comment in the middle -->Instead: If theme adjustments are needed, modify the component's SkinID parameter or adjust CSS in .css files.
If you copy themes but don't use <ThemeProvider> in the layout, CSS is never injected:
<!-- ❌ WRONG: Themes copied but never injected -->
<body>
@Body
</body>Instead: Wrap content with <ThemeProvider>:
<!-- ✅ RIGHT: CSS automatically injected -->
<ThemeProvider>
@Body
</ThemeProvider>Themes only apply inside <ThemeProvider>. Components outside the provider do not receive theme defaults:
<!-- ✅ RIGHT: Components inside ThemeProvider get theme defaults -->
<ThemeProvider>
<Button Text="Has theme" /> <!-- Theme applied -->
</ThemeProvider>
<!-- Outside ThemeProvider -->
<Button Text="No theme" /> <!-- No theme applied -->Do NOT skip the images/ subfolder when copying themes. If CSS or components reference theme images, they will break:
wwwroot/App_Themes/MyTheme/
images/
button-bg.png <!-- ✅ MUST copy this -->
icon.svg <!-- ✅ MUST copy this -->Last Updated: 2025-01-27 (Cyclops theme auto-discovery implementation)
1bd9b17
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.